﻿namespace JoinBox.Basal;

using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;

public class Shell32
{
    #region Directory
    /// <summary>
    /// 打开文件夹并选择项目
    /// </summary>
    [DllImport("shell32.dll", CharSet = CharSet.Auto)]
    public static extern int SHOpenFolderAndSelectItems(IntPtr pidlFolder, uint cidl, [MarshalAs(UnmanagedType.LPArray)] IntPtr[] apidl, uint dwFlags);

    /// <summary>
    /// 解析显示名称
    /// </summary>
    [DllImport("shell32.dll", CharSet = CharSet.Auto)]
    public static extern void SHParseDisplayName([MarshalAs(UnmanagedType.LPWStr)] string name, IntPtr bindingContext, out IntPtr pidl, uint sfgaoIn, out uint psfgaoOut);

    // https://stackoverflow.com/questions/8182494/how-can-i-set-an-existing-explorer-exe-instance-to-select-a-file
    /// <summary>
    /// 置前文件夹并设置文件焦点
    /// </summary>
    /// <param name="file">文件路径</param>
    public static void ShowFileInExplorer(string file)
    {
        // 如果是我自己开的同路径文件夹,它的表现是自己开一个新的.
        // 但是我用它开的,然后换了焦点,它则不开新的,而是重设焦点了.
        // 就好像有个缓存,它不是在全屏幕检索的,是在它第一次开了的文件夹上面检索的.

        // 获取当前打开的文件夹(资源管理器)指针
        var directory = Path.GetDirectoryName(file);
        if (directory == null)
            return;
        SHParseDisplayName(directory, IntPtr.Zero, out IntPtr folderPtr, 0, out _);
        if (folderPtr == IntPtr.Zero)
            return;
        if (File.Exists(file))
        {
            file = Path.GetFileName(file);
            SHParseDisplayName(file, IntPtr.Zero, out IntPtr filePtr, 0, out _);
            if (filePtr != IntPtr.Zero)
            {
                IntPtr[] files = { filePtr };
                SHOpenFolderAndSelectItems(folderPtr, (uint)files.Length, files, 0);
                Marshal.FreeCoTaskMem(filePtr);
            }
        }
        Marshal.FreeCoTaskMem(folderPtr);
    } 
    #endregion

    #region File
    /// <summary>
    /// 获取文件信息
    /// </summary>
    [DllImport("shell32.dll")]
    public static extern int SHGetFileInfoW(IntPtr pidl, uint dwFileAttributes, out SHFILEINFO psfi, uint cbFileInfo, uint flags);

    /// <summary>
    /// 获取文件夹路径
    /// </summary>
    [DllImport("shell32.dll")]
    public static extern HResult SHGetFolderLocation(IntPtr hwnd, int nFolder, IntPtr token, int dwReserved, out IntPtr pidl);

    /// <summary>
    /// 释放指针
    /// </summary>
    /// <param name="pidl"></param>
    [DllImport("shell32.dll")]
    public static extern void ILFree(IntPtr pidl);

    /// <summary>
    /// 获取特殊文件夹枚举值(如"桌面","文档")的中文名称
    /// </summary>
    public static string? GetDisplayInvoke(Environment.SpecialFolder specialFolder)
    {
        IntPtr pidl = IntPtr.Zero;
        try
        {
            HResult hr = SHGetFolderLocation(IntPtr.Zero, (int)specialFolder, IntPtr.Zero, 0, out pidl);
            if (hr.IsFailure)
                return null;

            if (0 != SHGetFileInfoW(pidl,
                                    FILE_ATTRIBUTE_NORMAL,
                                    out SHFILEINFO shfi,
                                    (uint)Marshal.SizeOf(typeof(SHFILEINFO)),
                                    SHGFI_PIDL | SHGFI_DISPLAYNAME))
            {
                return shfi.szDisplayName;
            }
            return null;
        }
        finally
        {
            if (pidl != IntPtr.Zero)
                ILFree(pidl);
        }
    }

    /// <summary>
    /// 获取所有的特殊文件夹
    /// </summary>
    /// <returns></returns>
    public static Dictionary<Environment.SpecialFolder, string> GetDisplayDictionary()
    {
        var dic = new Dictionary<Environment.SpecialFolder, string>();
        foreach (var value in Enum.GetValues(typeof(Environment.SpecialFolder)))
        {
            var spe = (Environment.SpecialFolder)value;  // 然后强转成枚举类型,可作为输出
            var name = GetDisplayInvoke(spe);
            if (name is not null && !dic.ContainsKey(spe))
                dic.Add(spe, name);
        }
        return dic;
    }

    /// <summary>
    /// 通过特殊文件夹的名称获取对应的枚举
    /// </summary>
    /// <returns></returns>
    public static Environment.SpecialFolder? GetDisplayInvoke(string specialFolder)
    {
        var dic = GetDisplayDictionary();
        foreach (var pa in dic)
        {
            if (pa.Value == specialFolder)
                return pa.Key;
        }
        return null;
    }

    public class GetDisplays
    {
        readonly Dictionary<Environment.SpecialFolder, string> dic;
        public GetDisplays()
        {
            dic = GetDisplayDictionary();
        }
        // 返回名称
        public string GetName(Environment.SpecialFolder specialFolder)
        {
            // 透过缓存速度更快
            return dic[specialFolder];
        }
        // 返回枚举
        public Environment.SpecialFolder? GetName(string specialFolder)
        {
            foreach (var pa in dic)
            {
                if (pa.Value == specialFolder)
                    return pa.Key;
            }
            return null;
        }
        // 窗口名称如果是特殊文件夹则不是路径,如"桌面"/"文档"/"音乐"
        // 返回路径
        public string? GetPath(string specialFolder)
        {
            string? path = null;
            var sf = GetName(specialFolder);
            if (sf is not null)
                path = Environment.GetFolderPath(sf.Value);
            return path;
        }
    }

    private const uint FILE_ATTRIBUTE_NORMAL = 0x00000080;
    private const uint SHGFI_DISPLAYNAME = 0x000000200; // get display name
    private const uint SHGFI_PIDL = 0x000000008;        // pszPath is a pidl

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
    public struct SHFILEINFO
    {
        public IntPtr hIcon;
        public int iIcon;
        public uint dwAttributes;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
        public string szDisplayName;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 80)]
        public string szTypeName;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct HResult
    {
        public int Value { get; }
        public Exception Exception => Marshal.GetExceptionForHR(Value)!;
        public bool IsSuccess => Value >= 0;
        public bool IsFailure => Value < 0;
    }
    #endregion
}